﻿using System;
using System.Linq;
using System.Collections.Generic;
using System.Text;
using System.Drawing;
using MLRobotic.Tower.Common;

namespace MLRobotic.Tower.Detection
{
    public class Clustering
    {
        private Log _log = new Log("Clustering");

        private int _width;
        private int _height;
        private int _bytesPerPixel;
        private int _windowsSize;
        private int _threshold;

        private int _stride;

        private int _cols;
        private int _rows;

        public Clustering(int width, int height, int bytesPerPixel, int windowsSize, int threshold)
        {
            if (windowsSize == 0)
                throw new ArgumentException("windowsSize cannot be 0");

            if (windowsSize > width || windowsSize > height)
                throw new ArgumentException("windowsSize must be smaller than width and height");

            if (threshold > windowsSize * windowsSize)
                throw new ArgumentException("threshold must be between 0 and windowsSize ^ 2");

            _width = width;
            _height = height;
            _bytesPerPixel = bytesPerPixel;
            _windowsSize = windowsSize;
            _threshold = threshold;

            _stride = _width * _bytesPerPixel;

            _cols = _width / _windowsSize;
            _rows = _height / _windowsSize;
        }

        public int Count(byte[] mask, byte[] key, Rectangle analyzeArea)
        {
            //_log.Debug("Count");

            if (mask.Length != _stride * _height)
                throw new ArgumentException("invalid mask size");

            int count = 0;
            for (int j = analyzeArea.Y; j < analyzeArea.Y + analyzeArea.Height; j++)
            {
                for (int i = analyzeArea.X; i < analyzeArea.X + analyzeArea.Width; i++)
                {
                    int index = j * _stride + i * _bytesPerPixel;
                    if (MathKey(mask, index, key))
                        count++;
                }
            }

            return count;
        }

        public IEnumerable<Rectangle> FindClusters(byte[] mask, byte[] key, Rectangle analyzeArea)
        {
            _log.DebugStartWatch("FindClusters");

            if (mask.Length != _stride * _height)
                throw new ArgumentException("invalid mask size");

            bool[,] explored = new bool[_cols, _rows];

            var clusters = new List<IList<Point>>();

            for (int i = analyzeArea.X / _windowsSize; i < (analyzeArea.X + analyzeArea.Width) / _windowsSize; i++)
            {
                for (int j = analyzeArea.Y / _windowsSize; j < (analyzeArea.Y + analyzeArea.Height) / _windowsSize; j++)
                {
                    var item = TryCreateCluster(mask, new Point(i, j), explored, key);
                    if (item != null)
                        clusters.Add(item);
                }
            }

            var results = new List<Rectangle>();
            foreach (var cluster in clusters)
            {
                int xMin = cluster[0].X;
                int yMin = cluster[0].Y;
                int xMax = cluster[0].X;
                int yMax = cluster[0].Y;

                foreach (var point in cluster)
                {
                    if (point.X < xMin)
                        xMin = point.X;

                    if (point.Y < yMin)
                        yMin = point.Y;

                    if (point.X > xMax)
                        xMax = point.X;

                    if (point.Y > yMax)
                        yMax = point.Y;
                }

                results.Add(new Rectangle(
                    xMin * _windowsSize,
                    yMin * _windowsSize,
                    (xMax - xMin + 1) * _windowsSize,
                    (yMax - yMin + 1) * _windowsSize));
            }

            _log.DebugElapsed("FindClusters");

            return results;
        }

        private IList<Point> TryCreateCluster(byte[] mask, Point startWindow, bool[,] explored, byte[] key)
        {
            List<Point> cluster = null;

            var toExplore = new List<Point>();
            toExplore.Add(startWindow);

            while (toExplore.Count > 0)
            {
                var window = toExplore[0];
                toExplore.RemoveAt(0);

                if (!explored[window.X, window.Y])
                {
                    explored[window.X, window.Y] = true;

                    if (ReachTreshold(mask, window, key))
                    {
                        if (cluster == null)
                            cluster = new List<Point>();

                        cluster.Add(window);

                        toExplore.AddRange(FindNeighboors(window));
                    }
                }
            }

            return cluster;
        }

        private bool ReachTreshold(byte[] mask, Point window, byte[] key)
        {
            int count = 0;
            for (int a = 0; a < _windowsSize; a++)
            {
                for (int b = 0; b < _windowsSize; b++)
                {
                    int index = (window.Y * _windowsSize + b) * _stride + (window.X * _windowsSize + a) * _bytesPerPixel;
                    if (MathKey(mask, index, key))
                    {
                        count++;
                        if (count >= _threshold)
                            return true;
                    }
                }
            }

            return false;
        }

        private bool MathKey(byte[] mask, int index, byte[] key)
        {
            for (int i = 0; i < _bytesPerPixel; i++)
            {
                if (mask[index + i] != key[i])
                    return false;
            }

            return true;
        }

        //private bool IsNotBlack(byte[] mask, int index)
        //{
        //    for (int i = 0; i < _bytesPerPixel; i++)
        //    {
        //        if (mask[index + i] != 0)
        //            return true;
        //    }

        //    return false;
        //}

        private IEnumerable<Point> FindNeighboors(Point window)
        {
            if (window.X > 0)
                yield return new Point(window.X - 1, window.Y);

            if (window.Y > 0)
                yield return new Point(window.X, window.Y - 1);

            if (window.X + 1 < _cols)
                yield return new Point(window.X + 1, window.Y);

            if (window.Y + 1 < _rows)
                yield return new Point(window.X, window.Y + 1);
        }
    }
}
